﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Tagging;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Text;


namespace PostscriptTools.Intellisense {
	[Export(typeof(ITaggerProvider))]
	[TagType(typeof(IOutliningRegionTag))]
	[ContentType("postscript")]
	internal sealed class OutliningTaggerProvider : ITaggerProvider {

		[Import]
		internal IBufferTagAggregatorFactoryService AggregatorFactory = null;

		public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag {
			//create a single tagger for each buffer.
			Func<ITagger<T>> sc = delegate() { return new OutliningTagger(buffer, this.AggregatorFactory) as ITagger<T>; };
			return buffer.Properties.GetOrCreateSingletonProperty<ITagger<T>>(sc);
		}

	}
	internal sealed class OutliningTagger : ITagger<IOutliningRegionTag>, IDisposable {
		const string Ellipsis = "...";    //the characters that are displayed when the region is collapsed
		readonly ITextBuffer _buffer;
		ITextSnapshot _snapshot;
		List<Region> _regions; // tuple(region, regionname)
		readonly ITagAggregator<PostscriptTokenTag> _aggregator;
		private readonly LazyEventHandler<TextContentChangedEventArgs> _lazyEventHandler;

		public OutliningTagger(ITextBuffer buffer, IBufferTagAggregatorFactoryService aggregatorFactory) {
			this._buffer = buffer;
			this._snapshot = buffer.CurrentSnapshot;
			this._regions = new List<Region>();
			int UpdateInterval = 500;
			if (_snapshot.Length > 10000) {
				UpdateInterval = 10*1000;
			}
			this._lazyEventHandler = new LazyEventHandler<TextContentChangedEventArgs>(BufferChanged, UpdateInterval, "outlining", allCallbacksMustBeDone: false);
			_buffer.Changed += this._lazyEventHandler.EventHandler;
			_aggregator = aggregatorFactory.CreateTagAggregator<PostscriptTokenTag>(buffer);
			ReParse();
		}
		public void Dispose() {
			_buffer.Changed -= this._lazyEventHandler.EventHandler;
		}


		// the Classifier tagger is translating buffer change events into TagsChanged events, so we don't have to
		public event EventHandler<SnapshotSpanEventArgs> TagsChanged;

		public IEnumerable<ITagSpan<IOutliningRegionTag>> GetTags(NormalizedSnapshotSpanCollection spans) {
			if (spans.Count == 0 || this._buffer.CurrentSnapshot.Length == 0) {
				//there is no content in the buffer
				yield break;
			}
			List<Region> currentRegions = this._regions;
			ITextSnapshot currentSnapshot = this._snapshot;
			SnapshotSpan entireSpansArgument = new SnapshotSpan(spans[0].Start, spans[spans.Count - 1].End).TranslateTo(currentSnapshot, SpanTrackingMode.EdgeExclusive); // a span that contains all of the spans in the argument
			foreach (Region region in currentRegions) {
				if (region.GetSpan().TranslateTo(currentSnapshot, SpanTrackingMode.EdgeInclusive).IntersectsWith(entireSpansArgument)) {
					var span = region.GetSpan().Span;
					int nrLines = currentSnapshot.GetLineNumberFromPosition(span.End) - currentSnapshot.GetLineNumberFromPosition(span.Start);
					string hint = region.GetSpan().Span.Length + " bytes, or " + nrLines + " Lines.";
					yield return new TagSpan<IOutliningRegionTag>(region.GetSpan(), new OutliningRegionTag(isDefaultCollapsed: region.DefaultIsCollapsed, isImplementation: region.IsImplementation, collapsedForm: Ellipsis, collapsedHintForm: hint)); // TODO: we could replace region.Contents with the contents of what was omitted, so we can glance at the contents
				}
			}
		}

		void BufferChanged(object sender, EventArgs eventArgs) {
			var e = eventArgs as TextContentChangedEventArgs;
			if (e == null) {
				return;
			}
			// If this isn't the most up-to-date version of the buffer, then ignore it for now (we'll eventually get another change event).
			if (e.After != this._buffer.CurrentSnapshot) {
				return;
			}
			ReParse();
		}



		void ReParse() {
			// we do not want to be called more than twice per second. Ignore superfluous requests because it slows down the editor too much
			// reparse the whole buffer again
			ITextSnapshot newSnapshot = this._buffer.CurrentSnapshot;
			var newRegions = new List<Region>();

			PartialRegion currentRegion = null; // stores the deepest started region, which has reference to the chain of parent regions
			var allOfnewSnapshot = new SnapshotSpan(newSnapshot, 0, newSnapshot.Length);
			IMappingTagSpan<PostscriptTokenTag> previousTagSpan = null;
			int currentProcedureLevel = 0; // { and } 
			int skippingOverPreprocessorTillLevel = 0;
			int currentPreprocessorLevel = 0;
			foreach (IMappingTagSpan<PostscriptTokenTag> currentTagSpan in _aggregator.GetTags(new NormalizedSnapshotSpanCollection(allOfnewSnapshot))) {
				if (currentTagSpan.Tag.Type == PostscriptToken.PsPreprocessorCommand) {
					// preprocessor stuff. 
					// #if and related commands #ifdef #ifndef #ifeq increment the level
					// #elif, #else, leave the level the same 
					// #endif decrements the level 
					// all other commands leave the level the same (#define, #//, #include #undef, ...)
					// If we see a #else or #elif, skip till corresponding #endif
					var ntag = currentTagSpan.Tag as PostscriptNamedTag;
					if (ntag.Name.StartsWith("if")) {
						currentPreprocessorLevel++;
					} else if (ntag.Name.StartsWith("end")) {
						currentPreprocessorLevel--;
						if (skippingOverPreprocessorTillLevel == currentPreprocessorLevel + 1) {
							skippingOverPreprocessorTillLevel = 0;
						}
					} else if (ntag.Name.StartsWith("else") || ntag.Name.StartsWith("elif")) {
						if (skippingOverPreprocessorTillLevel == 0) {
							// only if we are not yet skipping
							// skip till corresponding #endif
							Debug.Assert(currentPreprocessorLevel > 0); // since we incremented at #if
							skippingOverPreprocessorTillLevel = currentPreprocessorLevel;
						}
					}
					previousTagSpan = null;
					continue;
				}
				if (skippingOverPreprocessorTillLevel > 0) {
					continue;
				}
				if (currentTagSpan.Tag.Type == PostscriptToken.PsProcOpenOperator) {
					// { token
					if (currentProcedureLevel == 0) {
						if (currentRegion != null) {
							// previous region end
							SnapshotPoint? endOfImplementation = previousTagSpan.Span.Start.GetPoint(newSnapshot, PositionAffinity.Predecessor);
							newRegions.Add(new Region(currentRegion, endOfImplementation.Value));
						}
					}
					currentProcedureLevel++;
					if ((previousTagSpan != null) && (previousTagSpan.Tag.Type == PostscriptToken.PsLiteralName)) {
						// start of procedure (region starts after the { token)
						SnapshotPoint? startOfImplementation = currentTagSpan.Span.End.GetPoint(newSnapshot, PositionAffinity.Successor);
						var ntag = previousTagSpan.Tag as PostscriptNamedTag;
						currentRegion = new PartialRegion() {BodyAtLevel = currentProcedureLevel, StartPoint = startOfImplementation.Value, PartialParent = currentRegion, Contents = ntag.Name, DefaultIsCollapsed = false, IsImplementation = true};
					} else {
						// open { but no proc name
						// if we were to use this, we would specify IsImplementation = false
					}
				} else if ((currentTagSpan.Tag.Type == PostscriptToken.PsProcCloseOperator)) {
					currentProcedureLevel--;
					if (currentProcedureLevel < 0) {
						// we have more closes than opens
						if (currentRegion != null) {
							// I do not think we can get here: a close closes the nearest partial region
							// end previous region after the }
							SnapshotPoint? endOfImplementation = currentTagSpan.Span.Start.GetPoint(newSnapshot, PositionAffinity.Predecessor);
							newRegions.Add(new Region(currentRegion, endOfImplementation.Value));
						} else {
							// end proc for which no open was found: ignore
						}
						currentProcedureLevel = 0; // reset level
					} else {
						// regular close of current region
						if (currentRegion != null) {
							if (currentRegion.BodyAtLevel == currentProcedureLevel + 1) {
								SnapshotPoint? endOfImplementation = currentTagSpan.Span.Start.GetPoint(newSnapshot, PositionAffinity.Predecessor);
								newRegions.Add(new Region(currentRegion, endOfImplementation.Value));
								currentRegion = currentRegion.PartialParent;
							} else {
								// close of non named proc at level > 0
							}
						} else {
							Debug.Assert(currentProcedureLevel == 0); // close of non named proc at top level
						}
					}
				}
				previousTagSpan = currentTagSpan;
			}
			// if we have a partial region here, we close it and all containing regions.
			while (currentRegion != null) {
				SnapshotPoint? endOfImplementation = previousTagSpan.Span.End.GetPoint(newSnapshot, PositionAffinity.Successor);
				newRegions.Add(new Region(currentRegion, endOfImplementation.Value));
				currentRegion = currentRegion.PartialParent;
			}

			var oldSpans = new List<Span>(
				this._regions.Select(r => r.GetSpan()
											.TranslateTo(newSnapshot, SpanTrackingMode.EdgeExclusive)
											.Span)
				);
			this._snapshot = newSnapshot;
			this._regions = newRegions;

			if (this.TagsChanged != null) {
				//determine the changed span, and send a changed event with the new spans
				var newSpans = new List<Span>(newRegions.Select(r => r.GetSpan().Span));
				var oldSpanCollection = new NormalizedSpanCollection(oldSpans);
				var newSpanCollection = new NormalizedSpanCollection(newSpans);
				//the changed regions are regions that appear in one set or the other, but not both.
				NormalizedSpanCollection removed = NormalizedSpanCollection.Difference(oldSpanCollection, newSpanCollection);
				int changeStart = int.MaxValue;
				int changeEnd = -1;
				if (removed.Count > 0) {
					changeStart = removed[0].Start;
					changeEnd = removed[removed.Count - 1].End;
				}
				if (newSpans.Count > 0) {
					changeStart = Math.Min(changeStart, newSpans[0].Start);
					changeEnd = Math.Max(changeEnd, newSpans[newSpans.Count - 1].End);
				}
				if (changeStart <= changeEnd) {
					this.TagsChanged(this, new SnapshotSpanEventArgs(new SnapshotSpan(this._snapshot, Span.FromBounds(changeStart, changeEnd))));
				}
			}
		}


		class PartialRegion {
			public SnapshotPoint StartPoint { get; set; }
			public int BodyAtLevel { get; set; } // the region itself it at BodyAtLevel - 1
			public PartialRegion PartialParent { get; set; }
			public string Contents { get; set; }
			public bool DefaultIsCollapsed { get; set; }
			public bool IsImplementation { get; set; }
		}

		class Region : PartialRegion {
			public Region(PartialRegion basis, SnapshotPoint endpoint) {
				StartPoint = basis.StartPoint;
				BodyAtLevel = basis.BodyAtLevel;
				PartialParent = basis.PartialParent;
				Contents = basis.Contents;
				DefaultIsCollapsed = basis.DefaultIsCollapsed;
				IsImplementation = basis.IsImplementation;
				EndPoint = endpoint;
			}
			public SnapshotPoint EndPoint { get; set; }
			public SnapshotSpan GetSpan() {
				return new SnapshotSpan(StartPoint, EndPoint);
			}
		}
	}
}